package bancosys.tec.persist.traverse.visitor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.persistence.Transient;

import jmine.tec.persist.PersistMessages;

import org.hibernate.proxy.HibernateProxy;

import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.utils.traverser.TraverseException;
import bancosys.tec.utils.traverser.Visitor;

/**
 * Visitor que transforma o objeto para upperCase
 * 
 * @author pedro
 */
public class UpperCaseVisitor implements Visitor {
    private boolean upperCaseEnabled;

    private static final String GET = "get";

    private static final String SET = "set";

    /**
     * Diz se o upper case está habilitado ou não
     * 
     * @return se o upperCase está habilitado ou não
     */
    public boolean isUpperCaseEnabled() {
        return this.upperCaseEnabled;
    }

    /**
     * Diz se o upperCase está habilitado ou não
     * 
     * @param upperCaseEnabled se o upperCase está habilitado ou não
     */
    public void setUpperCaseEnabled(boolean upperCaseEnabled) {
        this.upperCaseEnabled = upperCaseEnabled;
    }

    /**
     * Método que torna o objeto feito para upperCase
     * 
     * @param node objeto a ser transformado em upperCase
     * @throws TraverseException caso não possa transformar para upperCase
     */
    public void visit(Object node) throws TraverseException {
        if (this.isUpperCaseEnabled()) {
            this.changeFieldsToUpperCase(node);
        }
    }

    /**
     * Método que efetivamente muda o case
     * 
     * @param bean objeto a ter as strings mudadas
     * @throws TraverseException caso ocorra algum erro
     */
    private void changeFieldsToUpperCase(Object bean) throws TraverseException {
        Class<?> clazz = bean.getClass();
        if (bean instanceof HibernateProxy) {
            clazz = ((HibernateProxy) bean).getHibernateLazyInitializer().getPersistentClass();
        }
        for (Method method : clazz.getMethods()) {
            if (this.mustBeChanged(method, bean)) {
                this.changeToUpperCase(method, bean);
            }
        }
    }

    /**
     * Devolve <code>true</code> se campo deve ser alterado. Campo deve ser alterado se, é somente se, metodo de campo devolver String, for
     * um getter e nao tiver anotacao de Transient.
     * 
     * @param method Método que devolve campo no BO.
     * @param bean objeto que pode ou não ser mudado.
     * @return <code>true</code> se deve ser alterado, <code>false</code> se nao.
     */
    private boolean mustBeChanged(Method method, Object bean) {
        return this.returnsString(method) && this.isGetter(method) && !this.isTransient(method);
    }

    /**
     * Devolve <code>true</code> se metodo devolve String, <code>false</code> se nao.
     * 
     * @param method metodo correspondente.
     * @return <code>true</code> se metodo devolve String, <code>false</code> se nao.
     */
    private boolean returnsString(Method method) {
        return String.class.equals(method.getReturnType());
    }

    /**
     * Devolve <code>true</code> se metodo é um getter.
     * 
     * @param method método correspondente.
     * @return <code>true</code> se metodo é um getter.
     */
    private boolean isGetter(Method method) {
        return method.getName().startsWith(GET) && method.getParameterTypes().length == 0;
    }

    /**
     * Devolve <code>true</code> se metodo possui anotacao Transient.
     * 
     * @param method método correspondente.
     * @return <code>true</code> se metodo possui anotacao Transient.
     */
    private boolean isTransient(Method method) {
        return method.isAnnotationPresent(Transient.class);
    }

    /**
     * Muda o valor para UpperCase
     * 
     * @param method método a ser mudado
     * @param bean objeto persistivel
     * @throws TraverseException caso não consiga obter o valor
     */
    private void changeToUpperCase(Method method, Object bean) throws TraverseException {
        String currentValue = this.getCurrentValue(method, bean);

        if (currentValue != null) {
            Method setter;
            try {
                setter = this.getSetterRelated(bean, method);
                this.updateValue(setter, bean, currentValue.toUpperCase());
            } catch (TraverseException e) {
                // supondo que é apenas um valor de exibição
            }

        }
    }

    /**
     * Executa o setter com o valor já em UpperCase
     * 
     * @param setter método setter
     * @param bean objeto persistivel
     * @param newValue valor em caixa Alta
     */
    private void updateValue(Method setter, Object bean, String newValue) {
        try {
            setter.invoke(bean, newValue);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.ERROR_UPDATING_VALUE.create(), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.ERROR_UPDATING_VALUE.create(), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.ERROR_UPDATING_VALUE.create(), e.getTargetException());
        }
    }

    /**
     * Obtém o valor da propriedade persistida
     * 
     * @param method getter que devolve a string
     * @param bean Objeto relacionado
     * @return O valor da propriedade
     * @throws TraverseException Problemas ao invocar método
     */
    private String getCurrentValue(Method method, Object bean) throws TraverseException {
        String currentValue;
        try {
            currentValue = (String) method.invoke(bean);
        } catch (IllegalArgumentException e) {
            throw new TraverseException(PersistMessages.ERROR_CONVERTING_TO_UPPER_CASE.create(method.getName()), e);
        } catch (IllegalAccessException e) {
            throw new TraverseException(PersistMessages.ERROR_CONVERTING_TO_UPPER_CASE.create(method.getName()), e);
        } catch (InvocationTargetException e) {
            throw new TraverseException(PersistMessages.ERROR_CONVERTING_TO_UPPER_CASE.create(method.getName()), e);
        }
        return currentValue;
    }

    /**
     * Devolve o método setter relacionado com o getter supostamente recebido
     * 
     * @param bean Objeto
     * @param method getter supostamente recebido
     * @return o método setter correspondente
     * @throws TraverseException Método getter sem setter correspondente
     */
    private Method getSetterRelated(Object bean, Method method) throws TraverseException {
        String setterName = method.getName().replaceFirst(GET, SET);
        Method setterMethod = null;
        try {
            setterMethod = bean.getClass().getMethod(setterName, String.class);
        } catch (NoSuchMethodException e) {
            throw new TraverseException(PersistMessages.SETTER_METHOD_NOT_FOUND.create(setterName), e);
        }
        return setterMethod;
    }
}
